# 小红书的自动发稿
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.common.action_chains import ActionChains
from selenium.common.exceptions import ElementClickInterceptedException, TimeoutException
import time
import json
import os
import sys


def safe_print(*args, **kwargs):
    """将日志写到 stderr，避免污染 MCP stdio 协议；并在 GBK 等环境下忽略无法编码字符。"""
    sep = kwargs.get("sep", " ")
    end = kwargs.get("end", "\n")
    text = sep.join(str(a) for a in args)
    try:
        enc = getattr(sys.stderr, "encoding", None) or "utf-8"
        filtered = text.encode(enc, errors="ignore").decode(enc, errors="ignore")
        sys.stderr.write(filtered + ("" if end is None else end))
        try:
            sys.stderr.flush()
        except Exception:
            pass
    except Exception:
        # 兜底静默
        pass


class CookieManager:
    """Simple manager for multiple cookie files per account.

    Files are expected under the same directory as the module by default and
    named either `xiaohongshu_cookies.json` (default) or
    `xiaohongshu_cookies_<account>.json`.
    """
    def __init__(self, base_dir):
        self.base_dir = base_dir
        self.default_name = "xiaohongshu_cookies.json"

    def _path_for(self, account: str | None) -> str:
        if not account or account in ("default", ""):
            return os.path.join(self.base_dir, self.default_name)
        filename = f"xiaohongshu_cookies_{account}.json"
        return os.path.join(self.base_dir, filename)

    def list_accounts(self) -> list:
        files = []
        try:
            for fn in os.listdir(self.base_dir):
                if fn.startswith("xiaohongshu_cookies") and fn.endswith(".json"):
                    if fn == self.default_name:
                        files.append(None)
                    else:
                        # extract account suffix
                        suffix = fn[len("xiaohongshu_cookies_"):-5]
                        files.append(suffix)
        except Exception:
            pass
        return files

    def load_cookies(self, account: str | None = None) -> list:
        path = self._path_for(account)
        if not os.path.exists(path):
            return []
        try:
            with open(path, 'r', encoding='utf-8') as f:
                cookies = json.load(f)
                sanitized = []
                for c in cookies:
                    # Sanitize cookie dict to keys supported by selenium
                    nc = {}
                    if 'name' in c and 'value' in c:
                        nc['name'] = c['name']
                        nc['value'] = c['value']
                    else:
                        continue
                    for k in ('domain', 'path', 'expiry', 'secure', 'httpOnly'):
                        if k in c:
                            # selenium expects booleans for secure/httpOnly
                            if k in ('secure', 'httpOnly'):
                                try:
                                    nc[k] = bool(c[k])
                                except Exception:
                                    nc[k] = c[k]
                            else:
                                nc[k] = c[k]
                    sanitized.append(nc)
                return sanitized
        except Exception:
            return []

    def save_cookies(self, driver, account: str | None = None) -> None:
        path = self._path_for(account)
        try:
            cookies = driver.get_cookies()
            with open(path, 'w', encoding='utf-8') as f:
                json.dump(cookies, f)
        except Exception:
            pass


class XiaohongshuPoster:
    def __init__(self, path=os.path.dirname(os.path.abspath(__file__)), account: str | None = None):
        safe_print("[DEBUG] 使用修改后的 XiaohongshuPoster 类")
        safe_print(f"[DEBUG] 初始化时间: {time.time()}")
        #self.driver = webdriver.ChromiumEdge()#.Chrome()
        self.driver = webdriver.Chrome()
        self.wait = WebDriverWait(self.driver, 30)
        # 获取当前执行文件所在目录
        current_dir = path
        self.token_file = os.path.join(current_dir, "xiaohongshu_token.json")
        # cookie/account management
        self._cookie_manager = CookieManager(current_dir)
        self.account = account
        # token and cookies
        self.token = self._load_token()
        self._load_cookies()

    def _wait_overlays_gone(self, timeout=5):
        """等待常见 tooltip/气泡层消失或强制隐藏，避免点击被遮挡"""
        overlay_selectors = [
            ".short-note-tooltip-text-content",
            ".el-tooltip__popper",
            "[role='tooltip']",
            ".tooltip",
        ]
        # 尝试等待不可见
        end_time = time.time() + timeout
        while time.time() < end_time:
            try:
                any_visible = self.driver.execute_script(
                    "return Array.from(arguments[0]).some(s=>{const el=document.querySelector(s); if(!el) return false; const r=el.getBoundingClientRect(); const style=getComputedStyle(el); return style.visibility!=='hidden' && style.display!=='none' && r.width>0 && r.height>0;});",
                    overlay_selectors,
                )
            except Exception:
                any_visible = False
            if not any_visible:
                return True
            time.sleep(0.2)
        # 超时后尝试强制隐藏
        try:
            self.driver.execute_script(
                "arguments[0].forEach(s=>{document.querySelectorAll(s).forEach(el=>{el.style.setProperty('display','none','important'); el.style.setProperty('visibility','hidden','important');});});",
                overlay_selectors,
            )
        except Exception:
            pass
        return True

    def _scroll_into_view(self, element):
        try:
            self.driver.execute_script("arguments[0].scrollIntoView({block: 'center', inline: 'center'});", element)
            time.sleep(0.1)
        except Exception:
            pass

    def _safe_click_element(self, element, desc="element", timeout=8):
        """健壮点击：滚动+等待遮罩消失+常规点击+JS回退点击"""
        try:
            self._scroll_into_view(element)
            try:
                ActionChains(self.driver).move_to_element(element).pause(0.1).perform()
            except Exception:
                pass
            self._wait_overlays_gone(timeout=2)
            # 先尝试原生点击
            try:
                element.click()
                return True
            except ElementClickInterceptedException:
                # 发送 ESC 关闭可能的气泡
                try:
                    self.driver.switch_to.active_element.send_keys(Keys.ESCAPE)
                    time.sleep(0.2)
                except Exception:
                    pass
                self._wait_overlays_gone(timeout=2)
                # 再试一次原生点击
                try:
                    element.click()
                    return True
                except ElementClickInterceptedException:
                    pass
            # 最后使用 JS 点击
            try:
                self.driver.execute_script("arguments[0].click();", element)
                return True
            except Exception:
                pass
        except Exception:
            pass
        safe_print(f"警告: 点击 {desc} 失败，已跳过或后续可能异常")
        return False

    def _safe_click(self, by, selector, desc="element", timeout=30):
        el = self.wait.until(EC.presence_of_element_located((by, selector)))
        try:
            self.wait.until(EC.visibility_of(el))
        except TimeoutException:
            pass
        return self._safe_click_element(el, desc=desc, timeout=timeout)

    def _load_token(self):
        """从文件加载token"""
        if os.path.exists(self.token_file):
            try:
                with open(self.token_file, 'r') as f:
                    token_data = json.load(f)
                    # 检查token是否过期
                    if token_data.get('expire_time', 0) > time.time():
                        return token_data.get('token')
            except:
                pass
        return None

    def _save_token(self, token):
        """保存token到文件"""
        token_data = {
            'token': token,
            # token有效期设为30天
            'expire_time': time.time() + 30 * 24 * 3600
        }
        with open(self.token_file, 'w') as f:
            json.dump(token_data, f)

    def _load_cookies(self):
        """从文件加载cookies"""
        cookies = self._cookie_manager.load_cookies(self.account)
        if not cookies:
            return
        try:
            self.driver.get("https://creator.xiaohongshu.com")
            for cookie in cookies:
                try:
                    self.driver.add_cookie(cookie)
                except Exception:
                    # ignore per-cookie errors
                    pass
        except Exception:
            pass

    def _save_cookies(self):
        """保存cookies到文件"""
        try:
            self._cookie_manager.save_cookies(self.driver, self.account)
        except Exception:
            pass
    def login_to_publish(self,title, content, images=None,slow_mode=False):
        #self.driver.get("https://creator.xiaohongshu.com/publish/publish?from=menu")
        self._load_cookies()
        self.driver.refresh()
        self.driver.get("https://creator.xiaohongshu.com/publish/publish?from=menu")
        # 用显式等待替代固定 8 秒等待
        try:
            self.wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, ".publishBtn")))
        except Exception:
            # 兜底短暂等待，避免个别资源慢导致未命中
            time.sleep(1)
        if self.driver.current_url != "https://creator.xiaohongshu.com/publish/publish?from=menu":
            return False, "登录失败"
        #time.sleep(1)
        # 如果是发布视频，则不操作这一步
        # 切换到上传图文

        tabs = self.driver.find_elements(By.CSS_SELECTOR, ".creator-tab")
        if len(tabs) > 2:
            self._safe_click_element(tabs[2], desc="图文发布Tab")
        # 减少不必要的等待
        # # 输入标题和内容

        # 上传图片
        if images:
            upload_input = self.driver.find_element(By.CSS_SELECTOR, ".upload-input")
            # 将所有图片路径用\n连接成一个字符串一次性上传
            upload_input.send_keys('\n'.join(images))
            # 上传后短暂等待，非慢速模式显著缩短
            time.sleep(5 if slow_mode else 1)
        title = title[:20]
        title_input = self.wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, ".d-text")))
        try:
            title_input.clear()
        except Exception:
            pass
        title_input.send_keys(title)

        # 填写正文，容错多个选择器并增加兜底方案
        editor_selectors = [
            ".ql-editor",
            "div.ql-editor",
            "[contenteditable='true']",
        ]
        content_filled = False
        for sel in editor_selectors:
            try:
                content_input = self.wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, sel)))
                self._scroll_into_view(content_input)
                try:
                    ActionChains(self.driver).move_to_element(content_input).pause(0.1).click().perform()
                except Exception:
                    pass
                try:
                    # 尝试 Ctrl+A 清空后输入
                    content_input.send_keys(Keys.CONTROL, 'a')
                    content_input.send_keys(Keys.DELETE)
                except Exception:
                    pass
                content_input.send_keys(content)
                content_filled = True
                break
            except Exception:
                continue
        if not content_filled:
            # 兜底：用 JS 直接填充第一个 contenteditable
            try:
                self.driver.execute_script("var el=document.querySelector('[contenteditable=\'true\']'); if(el){ el.innerText=arguments[0]; el.dispatchEvent(new Event('input',{bubbles:true})); }", content)
                content_filled = True
            except Exception as e:
                safe_print(f"正文输入失败: {e}")

        # 发布：等待按钮可点击，尽量减少固定等待
        try:
            submit_btn = self.wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, ".publishBtn")))
        except Exception:
            submit_btn = self.driver.find_element(By.CSS_SELECTOR, ".publishBtn")
        if slow_mode:
            time.sleep(3)
        self._safe_click_element(submit_btn, desc="发布按钮")
        safe_print('发布成功')
        time.sleep(2)
        return True, "发布成功"

    def login_to_publish_video(self,title, content, videos=None,slow_mode=False):
        #self.driver.get("https://creator.xiaohongshu.com/publish/publish?from=menu")
        self._load_cookies()
        self.driver.refresh()
        self.driver.get("https://creator.xiaohongshu.com/publish/publish?from=menu")
        # 用显式等待替代固定 8 秒等待
        try:
            self.wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, ".publishBtn")))
        except Exception:
            time.sleep(1)
        if self.driver.current_url != "https://creator.xiaohongshu.com/publish/publish?from=menu":
            return False, "登录失败"


        # # 输入标题和内容
        if videos:
            upload_input = self.driver.find_element(By.CSS_SELECTOR, ".upload-input")
            # 将所有图片路径用\n连接成一个字符串一次性上传
            upload_input.send_keys('\n'.join(videos))
            # 视频上传更久：慢速模式 10s，否则 2s
            time.sleep(10 if slow_mode else 2)
        title = title[:20]
        title_input = self.wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, ".d-text")))
        try:
            title_input.clear()
        except Exception:
            pass
        title_input.send_keys(title)

        # 填写正文（同图文）
        editor_selectors = [
            ".ql-editor",
            "div.ql-editor",
            "[contenteditable='true']",
        ]
        content_filled = False
        for sel in editor_selectors:
            try:
                content_input = self.wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, sel)))
                self._scroll_into_view(content_input)
                try:
                    ActionChains(self.driver).move_to_element(content_input).pause(0.1).click().perform()
                except Exception:
                    pass
                try:
                    content_input.send_keys(Keys.CONTROL, 'a')
                    content_input.send_keys(Keys.DELETE)
                except Exception:
                    pass
                content_input.send_keys(content)
                content_filled = True
                break
            except Exception:
                continue
        if not content_filled:
            try:
                self.driver.execute_script("var el=document.querySelector('[contenteditable=\'true\']'); if(el){ el.innerText=arguments[0]; el.dispatchEvent(new Event('input',{bubbles:true})); }", content)
                content_filled = True
            except Exception as e:
                safe_print(f"正文输入失败: {e}")
        # 发布：等待按钮可点击，尽量减少固定等待
        try:
            submit_btn = self.wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, ".publishBtn")))
        except Exception:
            submit_btn = self.driver.find_element(By.CSS_SELECTOR, ".publishBtn")
        if slow_mode:
            time.sleep(2)
        self._safe_click_element(submit_btn, desc="发布按钮")
        safe_print('发布成功')
        time.sleep(2)
        return True, "发布成功"
    def login(self, phone, country_code="+86"):
        """登录小红书"""
        # 如果token有效则直接返回
        if self.token:
            return

        # 尝试加载cookies进行登录
        self.driver.get("https://creator.xiaohongshu.com/login")
        self._load_cookies()
        self.driver.refresh()
        time.sleep(3)
        # 检查是否已经登录
        if self.driver.current_url != "https://creator.xiaohongshu.com/login":
            safe_print("使用cookies登录成功")
            self.token = self._load_token()
            self._save_cookies()
            time.sleep(2)
            return
        else:
            # 清理无效的cookies
            self.driver.delete_all_cookies()
            safe_print("无效的cookies，已清理")

        # 如果cookies登录失败，则进行手动登录
        self.driver.get("https://creator.xiaohongshu.com/login")

        # 等待登录页面加载完成
        time.sleep(5)
        # 点击国家区号输入框
        skip = True
        if not skip:
            country_input = self.wait.until(
                EC.presence_of_element_located((By.CSS_SELECTOR, "input[placeholder='请选择选项']")))
            country_input.click()
            time.sleep(30)

            # 等待区号列表出现并点击+886
            # 等待区号列表出现并点击+86

            try:
                self.driver.find_element(By.XPATH,
                                         "/html/body/div[1]/div/div/div/div[2]/div[1]/div[2]/div/div/div/div/div/div[2]/div[1]/div[1]/div/div/div[1]/input").click()
                time.sleep(2)
                self.driver.find_element(By.XPATH,
                                         "/html/body/div[1]/div/div/div/div[2]/div[1]/div[2]/div/div/div/div/div/div[2]/div[1]/div[1]/div/div/div[1]/input").send_keys(
                    country_code)
                time.sleep(2)
                self.driver.find_element(By.XPATH, "/html/body/div[6]/div/div").click()
                # china_option = self.wait.until(EC.element_to_be_clickable((By.XPATH, "//div[contains(@class, 'css-cqcgee')]//div[contains(text(), '+86')]")))
                time.sleep(2)
            except Exception as e:
                safe_print("无法找到国家区号选项")
                safe_print(e)

        # 定位手机号输入框
        phone_input = self.wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, "input[placeholder='手机号']")))
        phone_input.clear()
        phone_input.send_keys(phone)

        # 点击发送验证码按钮
        try:
            send_code_btn = self.wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, ".css-uyobdj")))
            send_code_btn.click()
        except:
            # 尝试其他可能的选择器
            try:
                send_code_btn = self.wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, ".css-1vfl29")))
                send_code_btn.click()
            except:
                try:
                    send_code_btn = self.wait.until(
                        EC.element_to_be_clickable((By.XPATH, "//button[contains(text(),'发送验证码')]")))
                    send_code_btn.click()
                except:
                    safe_print("无法找到发送验证码按钮")

        # 输入验证码
        verification_code = input("请输入验证码: ")
        code_input = self.wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, "input[placeholder='验证码']")))
        code_input.clear()
        code_input.send_keys(verification_code)

        # 点击登录按钮
        login_button = self.wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, ".beer-login-btn")))
        login_button.click()

        # 等待登录成功,获取token
        time.sleep(3)
        # 保存cookies
        self._save_cookies()

        # 关闭浏览器
        # self.driver.quit()

        # print(f"获取到的token: {token}")

        # if token:
        #     self._save_token(token)
        #     self.token = token
        # else:
        #     print("未能获取到token")

    def post_article(self, title, content, images=None):
        """发布文章
        Args:
            title: 文章标题
            content: 文章内容
            images: 图片路径列表
        """
        # 如果token失效则重新登录

        # 设置token
        # self.driver.execute_script(f'localStorage.setItem("token", "{self.token}")')
        #time.sleep(3)
        #print("点击发布按钮")
        # 点击发布按钮
        publish_btn = self.wait.until(
            EC.presence_of_element_located((By.CSS_SELECTOR, ".btn.el-tooltip__trigger.el-tooltip__trigger")))
        self._safe_click_element(publish_btn, desc="发布入口按钮")

        # 如果是发布视频，则不操作这一步
        # 切换到上传图文
        time.sleep(3)
        tabs = self.driver.find_elements(By.CSS_SELECTOR, ".creator-tab")
        if len(tabs) > 1:
            self._safe_click_element(tabs[1], desc="图文发布Tab")
        time.sleep(2)
        # # 输入标题和内容
        # title_input = self.wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, ".title-input")))
        # content_input = self.driver.find_element(By.CSS_SELECTOR, ".content-input")

        # title_input.send_keys(title)
        # content_input.send_keys(content)

        # 上传图片
        if images:
            upload_input = self.driver.find_element(By.CSS_SELECTOR, ".upload-input")
            # 将所有图片路径用\n连接成一个字符串一次性上传
            upload_input.send_keys('\n'.join(images))
            time.sleep(1)
        time.sleep(2)
        title=title[:20]
        title_input = self.wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, ".d-text")))
        title_input.send_keys(title)

        # Start of Selection
        # Start of Selection
        safe_print(content)
        content_input = self.wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, ".ql-editor")))
        content_input.send_keys(content)
        # 发布
        time.sleep(2)
        submit_btn = self.driver.find_element(By.CSS_SELECTOR, ".publishBtn")
        self._safe_click_element(submit_btn, desc="发布按钮")
        safe_print('发布成功')
        time.sleep(2)

    def post_video_article(self, title, content, videos=None):
        """发布文章
        Args:
            title: 文章标题
            content: 文章内容
            videos: 视频路径列表
        """
        # 如果token失效则重新登录

        # 设置token
        # self.driver.execute_script(f'localStorage.setItem("token", "{self.token}")')
        time.sleep(3)
        #print("点击发布按钮")
        # 点击发布按钮
        publish_btn = self.wait.until(
            EC.presence_of_element_located((By.CSS_SELECTOR, ".btn.el-tooltip__trigger.el-tooltip__trigger")))
        self._safe_click_element(publish_btn, desc="发布入口按钮")

        # 如果是发布视频，则不操作这一步
        # 切换到上传图文
        time.sleep(3)
        # # 输入标题和内容
        # title_input = self.wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, ".title-input")))
        # content_input = self.driver.find_element(By.CSS_SELECTOR, ".content-input")

        # title_input.send_keys(title)
        # content_input.send_keys(content)

        # 上传图片
        if videos:
            upload_input = self.driver.find_element(By.CSS_SELECTOR, ".upload-input")
            # 将所有图片路径用\n连接成一个字符串一次性上传
            upload_input.send_keys('\n'.join(videos))
            time.sleep(1)
        time.sleep(3)
        title_input = self.wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, ".d-text")))
        title_input.send_keys(title)

        # Start of Selection
        # Start of Selection
        safe_print(content)
        content_input = self.wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, ".ql-editor")))
        content_input.send_keys(content)
        # 发布
        time.sleep(6)
        submit_btn = self.driver.find_element(By.CSS_SELECTOR, ".publishBtn")
        self._safe_click_element(submit_btn, desc="发布按钮")
        safe_print('发布成功')
        time.sleep(3)


    def close(self):
        """关闭浏览器"""
        self.driver.quit()

